Shell学习笔记18-Expect自动化交互式程序

GO


1. Expect介绍

1.1. 什么是Expect

Expect是一个用来实现自动交互功能的软件套件(Expect is a softeare suite for automating interactive tools,这是作者的定义),是基于TCL(全拼为Tool Command Language,是一种脚本语言,由John Ousterhout创建。TCL功能很强大,经常被用于快速原型开发、脚本编程、GUI和测试等方面,不过现在用得不多了)的脚本编程工具语言,方便学习,功能强大。

1.2. 为什么要使用Expect

在现今的企业运维中,自动化运维已经称为运维的主流趋势,但是在很多情况下,执行系统命令或程序时,系统会以交互式的形式要求运维人员输入指定的字符串,之后才能继续执行命令。例如,为用户设置密码时,一般情况下就需要手工输入2次密码;再比如使用SSH远程连接服务器时,第一次连接要和系统实现两次交互式输入。

简单的说,Expect就是用来自动实现与交互式程序通信的,而无需管理员的手工干预。比如SSH、FTP远程连接等,正常情况下都需要手工与它们进行交互,而使用Except就可以模拟手工交互的过程,实现自动与远端程序的交互,从而达到自动化运维的目的。

以下是Except的自动交互工作流程的简单说明,依次执行如下操作:

spawn启动指定进程—->expect获取期待的关键字—->send向指定进程发送指定字符—->进程执行完毕,退出结束。

2. 安装Expect软件

首先,要确保机器可以正常上网,并设置好yum安装源,然后执行yum install expect -y命令安装Except软件,安装过程如下:

1
2
3
4
[root@theshu ~]# rpm -qa expect #<==检测是否安装
[root@theshu ~]# yum install expect -y #<==若没有安装则安装
[root@theshu ~]# rpm -qa expect #<==再次检查
expect-5.45-14.el7_1.x86_64

3. 小试牛刀:实现Expect自动交互功能

  • 编写theshu.exp #<==扩展名使用exp代表是Expect脚本

    1
    2
    3
    4
    5
    #!/usr/bin/expect #<==脚本开头解释器,和Shell类似,表示程序使用Expect解析
    spawn ssh root@192.168.33.130 uptime #<==实行ssh命令(注意开头必须要有spawn,否则无法实现交互)
    expect "*password" #<==利用Except获取执行上述ssh命令输出的字符串是否为期待的字符串*password,这里的*是通配符
    send "123456\n" #<==当获取到期待的字符串*password时,则发送12345密码给系统。\n为换行。
    expect eof #<==处理完毕后结束Except
  • 执行Expect:expect theshu.exp

4. Expect程序自动交互的重要命令及实践

Expect程序中的命令是Expect的核心,需要重点掌握。

4.1. spawn命令

在Expect自动交互程序执行的过程中,spawn命令是一开始就需要使用的命令,通过spawn执行一个命令或程序,之后所有的Expect操作都会在这个执行过的命令或程序进程中进行,包括自动交互功能,因此如果没有spawn命令,Expect程序将会无法实现自动交互。

spawn命令的语法为:spawn [选项] [需要自动交互的命令或程序]

例如:spawn ssh root@192.168.33.130 uptime

spawn命令的后面,直接加上要执行的命令或程序(例如这里的ssh命令)等,除此之外,spawn还支持如下一些选项:

  • -open:表示启动文件进程
  • -ignore:表示忽略某些信号
  • 提示:这些选项不常用,了解即可,无需深入。

使用spawn命令是Expect程序实现自动交互工作流程中的第一步,也是最关键的一步。

4.2. expect命令

4.2.1. expect命令语法

在Expect自动交互程序的执行过程中,当使用spawn命令执行一个命令或程序之后,会提示某些交互式信息,expect命令的作用就是获取spawn命令执行后的信息,看看是否和其事先指定的相匹配,一旦匹配上指定的内容就执行expect后面的动作,expect命令也有一些选项,相对用得多的是-re,表示使用正则表达式的方式来匹配。

expect命令的语法为:expect 表达式 [动作]

示例如下:

1
2
spawn ssh root@192.168.33.130 uptime
expect "*password" {send "123456\r"}

注意,上述命令不能直接在Linux命令行中执行,需要放入Expect脚本中执行。

4.2.2. expect命令的实践

范例1:执行ssh命令远程获取服务器负载值,并要求实现自动输入密码。

  • 方法1:将expectsend放在一行:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [root@theshu ~]# which expect
    /bin/expect #<==expect程序的绝对路径
    [root@theshu ~]# cat test1.exp
    #!/bin/expect #<==指定expect解释器
    spawn ssh theshu@67.216.210.110 -p 29643 uptime #<==开启expect自动交互式,执行ssh命令
    expect "*password" {send "theshu..\n"} #<==如果ssh命令输出匹配*oassword,就发送theshu..给系统
    expect eof #<==要想输出结果,还必须加eof,表示expect结束
    [root@theshu ~]# expect test1.exp #<==采用expect执行脚本,就相当于使用sh执行Shell脚本
    spawn ssh theshu@67.216.210.110 -p 29643 uptime
    theshu@67.216.210.110's password: #<==Expect程序自动帮我们输入了密码
    07:56:00 up 33 days, 1:39, 0 users, load average: 0.00, 0.00, 0.00

从上面的例子可以看出,expect命令是依附于spawn命令的,即通过spawn执行ssh命令后,系统会提示输入密码,此时的expect命令按照事先的配置匹配ssh命令执行后的字符串password,如果匹配到了指定的passeord字符串,则会执行紧随其后包含在大括号中的sendexp_send动作,匹配的动作也可以放在下一行,这样就不需要使用大括号了,就像下面这样,实际完成的功能与上面的一样。

  • 方法2:expectsend放在不同行
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [root@theshu ~]# cat test2.exp
    #!/bin/expect
    spawn ssh theshu@67.216.210.110 -p 29643 uptime
    expect "*password"
    send "theshu..\n"
    expect eof
    [root@theshu ~]# expect test2.exp
    spawn ssh theshu@67.216.210.110 -p 29643 uptime
    theshu@67.216.210.110's password:
    08:04:38 up 33 days, 1:47, 0 users, load average: 0.00, 0.00, 0.00

expect命令还有一种高级用法,即它可以在一个expect匹配中多次匹配不同的字符串,并给出不通的处理动作,此时只需要将匹配的所有字符串放在一个大括号里就可以了,当然还要借助exp_continue指令实现继续匹配。

范例3:执行ssh命令远程获取服务器负载值,并自动输入yes及用户密码。

1
2
3
4
5
6
7
8
[root@theshu ~]# cat test3.exp
#!/bin/expect
spawn ssh theshu@67.216.210.110 -p 29643 uptime
expect { #<==起始大括号前要有空格
"yes/no" {exp_send "yes\r";exp_continue} #<==exp_send和send类似
"*password" {exp_send "theshu..\r"}
}
expect eof

说明:

  • exp_sendsed类似,后面的\r(回车)和前文的\n(换行)类似。
  • expect{},类似多行expect
  • 匹配多个字符串,需要在每次匹配并执行动作后,加上exp_continue

4.3. send命令

sendexp_send这两个命令是Expect中的动作命令,用法类似,即在expect命令匹配指定的字符串后,发送指定的字符串给系统,这些命令可以支持一些特殊的转义符号。例如:\r表示回车、\n表示换成、\t表示制表符等,这些用法与TCL中的特殊符号相同。

示例如下:

1
2
3
4
5
6
7
8
#!/bin/expect
spawn /bin/sh test.sh
expect {
"username" {exp_send "theshu\r"; exp_continue}
"*pass*" {send "123456\r"; exp_continue}
"*mail*" {exp_send "theshu@qq.com\r"}
}
expect eof

send命令有几个可用的参数,具体如下。

  • -i:指定spawn_id,用来向不通的spawn_id进程发送命令,是进行多程序控制的参数。
  • -s:s代表slowly,即控制发送的速度,使用的时候要与expect中的变量send_slow相关联。

4.4. exp_continue命令

exp_continue命令,一般处于expect命令中,属于一种动作命令,一般用在匹配多次字符串的动作中,从命令的拼写就可以看出命令的作用,即让Expect程序继续匹配的意思,示例如下:

1
2
3
4
5
6
7
8
#!/bin/expect
spawn /bin/sh test.sh
expect {
"username" {exp_send "theshu\r"; exp_continue}
"*pass*" {send "123456\r"; exp_continue}
"*mail*" {exp_send "theshu@qq.com\r"}
}
expect eof

说明:如果需要一次匹配多个字符串,那么不同的匹配之间就要加上exp_continue,否则expect将不会自动输入指定的字符串。最后一个的结尾就不需要加上exp_continue了,因为都匹配完成了。

在这个例子中,匹配第一个字符串”username”之后,expect发送”theshu”字符串给系统,然后利用exp_continue继续匹配下一个字符串”*pass*“,并自动输入指定字符串”123456”,最后匹配”*mail*“,并输入指定字符串”theshu@qq.com”,由于结尾没有exp_continue,因此匹配结束。

4.5. send_user命令

send_user命令可以用来打印Expect脚本信息,类似Shell里的echo命令,而默认的sendexp_send命令都是将字符串输出到Expect程序中去,有关send_user命令用法的示例如下:

1
2
3
4
5
6
7
8
[root@theshu ~]# cat send_user.exp
#!/bin/expect
send_user "I am theshu.\n"
send_user "I am a Linuxer.\t"
send_user "My blog is http://www.theshu.top\n"
[root@theshu ~]# expect send_user.exp
I am theshu.
I am a Linuxer. My blog is http://www.theshu.top

通过上述例子可以看出,它很像Shell里的echo命令。而且有echo -e的功能。

4.6. exit命令

exit命令的功能类似于Shell中的exit,即直接退出Expect脚本,除了最基本的退出脚本功能之外,还可以利用这个命令对脚本做一些关闭前的清理和提示等工作,比如下面的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@theshu ~]# cat exit_test.exp
#!/bin/expect
send_user "I am theshu.\n"
send_user "I am a Linuxer,\t"
send_user "My blog is http://www.theshu.top.\n"
exit -onexit {
send_user "Good bye.\n"
}
[root@theshu ~]# expect exit_test.exp
I am theshu.
I am a Linuxer, My blog is http://www.theshu.top.
Good bye.

4.7. Expect常用命令总结

下面将上面所说的知识进行总结以方便记忆,整理结果如下标:

Expect命令 作用
spawn spawn命令是一个在Expect自动交互程序的开始就需要使用的命令,通过spawn执行一个命令或程序,之后所有的Expect操作都在这个执行过的命令或程序进程中进行,包括自动交互功能
expect 在Expect自动交互程序的执行过程中,在使用spawn命令执行一个命令或程序之后,会提示某些交互式信息,expect命令的作用就是获取这些信息,查看是否和其事先指定的信息相匹配,一旦匹配上指定的内容,就执行expect后面的动作
send Expect中的动作命令,当expect匹配了指定的字符串后,发送指定的字符串给系统,这些命令可以支持一些特殊的转义符号,例如\r表示回车,\n表示换行,\t表示制表符等,还有一个类似的exp_send命令
exp_continue 属于一种动作命令,在一个expect命令中,用于多次匹配字符串并执行不同的动作中。该命令的作用就是让expect程序继续匹配
send_user send_user命令用来打印Expect脚本信息,类似Shell里的echo命令,并且带上-e选项的功能
exit 退出Expect脚本,以及在退出脚本前做一些关闭前的清理和提示等工作

5. Expect程序变量

5.1. 普通变量

Expect中的变量定义、使用方法与TCL语言中的变量基本相同。

定义变量的基本语法如下:set 变量名 变量值。例如:set password "123456"

打印变量的基本语法如下:puts $变量名send_user "$变量名\n"

范例:定义及输出变量

1
2
3
4
5
6
7
8
[root@theshu ~]# cat 5.1.exp
#!/usr/bin/expect
set password "123456"
puts $password
send_user "$password\n"
[root@theshu ~]# expect 5.1.exp
123456
123456

5.2. 特殊参数变量

在Expect里也有与Shell脚本里的$0、$1、$#等类似的特殊参数变量,用于接收及控制Expect脚本传参。

在Expect中$argv表示参数数组,可以使用[lindex $argv n]接收Expect脚本传参,n从0开始,分别表示第一个[lindex $argv 0]参数、第二个[lindex $argv 1]参数、第三个[lindex $argv 2]参数……

范例1:定义及输出特殊参数变量:

1
2
3
4
5
6
7
8
9
[root@theshu ~]# cat 1.exp
#!/bin/expect
#define var
set file [lindex $argv 0]
set host [lindex $argv 1]
set dir [lindex $argv 2]
send_user "$file\t$host\t$dir\n"
[root@theshu ~]# expect 1.exp theshu.log 192.168.33.130 /tmp
theshu.log 192.168.33.130 /tmp

Expect接收参数的方式和bash脚本的方式有些区别,bash是通过$0…$n这种方式来接受的,而Expect是通过set 变量名称 [lindex $argv <param index>]来接收的,例如:set file [lindex $argv 0]

除了基本的位置参数外,Expect也支持其他的特殊参数,例如:$argc表示传参的个数,$argv0表示脚本的名字。

范例2:针对Expect脚本传参的个数及脚本名参数的实践

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@theshu ~]# cat 18-7-2.exp
#!/bin/expect
#define var
set file [lindex $argv 0]
set host [lindex $argv 1]
set dir [lindex $argv 2]
puts "$file\t$host\t$dir"
puts $argc
puts $argv0
[root@theshu ~]# expect 18-7-2.exp theshu.txt 10.0.0.3 /opt
theshu.txt 10.0.0.3 /opt
3
18-7-2.exp

6. Expect程序中的if条件语句

Expect程序中if条件语句的基本语法为:

1
2
3
if { 条件表达式 } {
指令
}


1
2
3
4
5
if { 条件表达式 } {
指令
} else {
指令
}

说明:if关键字后面要有空格,else关键字前后都要有空格,{条件表达式}大括号里面靠近大括号处可以没有空格,将指令括起来的起始大括号{前面要有空格。

范例1:使用if语句判断脚本传参的个数,如果不符则给予提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@theshu ~]# cat test.exp
#!/bin/expect
if { $argc != 3 } { #<==$argv0为传参的个数,相当于Shell里的$#
send_user "usage: expect $argv0 file host dir\n" #<==给予提示,$argv0代表脚本的名字
exit #<==退出脚本
}
#define var
set file [lindex $argv 0]
set host [lindex $argv 1]
set dir [lindex $argv 2]
puts "$file\t$host\t$dir"
[root@theshu ~]# expect test.exp #执行结果
usage: expect 18-9-1.exp file host dir
[root@theshu ~]# expect test.exp theshu.log 192.168.33.130 /home/theshu
theshu.log 192.168.33.130 /home/theshu

范例2:使用if语句判断脚本传参的个数,不管是否符合都给予提示。

1
2
3
4
5
6
7
8
9
10
11
[root@theshu ~]# cat 18-10.exp
#!/bin/expect
if {$argc != 26} {
puts "bad."
} else {
puts "good."
}
[root@theshu ~]# expect 18-10.exp
bad.
[root@theshu ~]# expect 18-10.exp {a..z}
good.

这篇学习笔记的目的并不是用来彻底精通Expect语言,而是指导读者解决运维管理中的交互问题,实现自动化运维,因此,不要过多地纠结于Expect语言,而应该多关注自动化交互的知识。

7. Expect中的关键字

Expect中的特殊关键字用于匹配过程,代表某些特殊的含义或状态,一般只用于Expect命令中而不能在Expect命令外面单独使用。

7.1. eof关键字

eof(end-of-file)关键字用于匹配结束符。例如:

1
2
3
4
#!/bin/expect
spawn ssh root@192.168.33.130 uptime
expect "*password" {send "123456\n"}
expect eof

再比如:

1
2
3
4
5
6
7
#!/bin/expect
spawn ssh root@192.168.33.130 uptime
expect {
"yes/no" {exp_send "yes\r"; exp_continue}
"*password" {exp_send "123456\r"}
}
expect eof

7.2. timeout关键字

timeout是Expect中的一个控制时间的关键字变量,它是一个全局性的时间控制开关,可以通过为这个变量赋值来规定整个Expect操作的时间,注意这个变量是服务于Expect全局的,而不是某一条命令,即使命令没有任何错误,到了时间仍然会激活这个变量,此外,到时间后还会激活一个处理及提示信息开关,下面来看看它的实际使用方法。

范例:timeout超时功能实践。

1
2
3
4
5
6
7
8
9
[root@theshu ~]# cat test.exp
#!/bin/bash
spawn ssh root@192.168.33.130 uptime
set timeout 30
expect "yes/no"
expect timeout
[root@theshu ~]# expect test.exp
spawn ssh root@192.168.33.130 uptime
root@192.168.33.130's password: Request timeout by theshu.

上面的处理中,首先将timeout变量设置为30秒,此时Expect脚本的执行只要超过了30秒,就会直接执行结尾的timieout动作,打印一个信息,停止运行脚本。还可以做更多的其他事情。

在expect{}的用法中,还可以使用下面的timeout语法:

1
2
3
4
5
6
7
8
9
10
11
[root@theshu ~]# cat test.exp
#!/bin/bash
spawn ssh root@192.168.33.130 uptime
expect {
-timeout 3
"yes/no" {exp_send "yes\r"; exp_continue}
timeout {puts "Request timeout by theshu."; return}
}
[root@theshu ~]# expect test.exp
spawn ssh root@192.168.33.130 uptime
root@192.168.33.130's password: Request timeout by theshu.

timeout变量设置为0,表示立即超时,为-1则表示永不超时。

8. 小结

Expect程序的功能远不止文本介绍的这些,本篇笔记主要从运维工作实战的角度,来讲解自动化运维中用Shell脚本难以实现的交互式问题的解决方案。对于一般的企业运维人员,掌握以上的这些就已经够用了。类似的交互工具还有sshpass、ansible等。


OK

0%